package com.demo.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

@Slf4j
public class SignUtil {

    //签名算法HmacSha256
    public static final String HMAC_SHA256 = "HmacSHA256";

    //换行符
    private static char LF = '\n';
    //编码
    private static final String ENCODING = "UTF-8";

    //HTTP POST
    private static final String HTTP_METHOD_POST = "post";
    //HTTP PUT
    private static final String HTTP_METHOD_PUT = "put";

    public static void main(String[] args) throws Exception{
        Map<String, String> headers = new HashMap<>();
        headers.put("Cc-Appid","应用ID");
        headers.put("Cc-Timestamp","1656483388878");
        headers.put("Cc-Nonce","f9272b4b-6795-4c59-bcca-5d690fdb087e");

        System.out.println(serviceSign("/data/user/getUserInfo","POST",headers,new HashMap<>(),"{\"access_token\":\"cb3be26e-355d-449a-a8f2-cee376fec75d\"}"
                ,"签名密钥"));
    }


    /**
     * 计算HTTP请求签名
     *
     * @param uri              原始HTTP请求PATH（不包含Query）
     * @param httpMethod       原始HTTP请求方法
     * @param headers          原始HTTP请求需要签名的请求头
     * @param paramsMap        原始HTTP请求所有Query+Form参数
     * @param body 原始HTTP请求Body体（仅当请求为POST/PUT且非表单请求才需要设置此属性,表单形式的需要将参数放到paramsMap中）
     * @param secretKey 签名密钥
     *
     * @return 签名结果
     * @throws Exception
     */
    public static String serviceSign(String uri, String httpMethod, Map<String, String> headers, Map<String, String> paramsMap, String body,String secretKey) throws Exception {
        //buildBodyMd5
        String bodyMd5 = buildBodyMd5(httpMethod, body);
        String resourceToSign = buildResource(uri, paramsMap);
        String stringToSign = buildStringToSign(headers, resourceToSign, httpMethod, bodyMd5);

        log.info("uri:{},stringToSign:\n{}\nbody:{}\nbodyMd5:{}",uri,stringToSign,body,bodyMd5);

        Mac hmacSha256 = Mac.getInstance(HMAC_SHA256);

        byte[] keyBytes = secretKey.getBytes(ENCODING);
        hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, HMAC_SHA256));

        return new String(Base64.encodeBase64(hmacSha256.doFinal(stringToSign.getBytes(ENCODING))), ENCODING);
    }

    /**
     * 构建BodyMd5
     *
     * @param httpMethod       HTTP请求方法
     * @param body HTTP请求Body体
     * @return Body Md5值
     * @throws IOException
     */
    private static String buildBodyMd5(String httpMethod, String body) throws IOException {
        if (StringUtils.isBlank(body)) {
            return null;
        }

        if (!httpMethod.equalsIgnoreCase(HTTP_METHOD_POST) && !httpMethod.equalsIgnoreCase(HTTP_METHOD_PUT)) {
            return null;
        }

        InputStream inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
        byte[] bodyBytes = IOUtils.toByteArray(inputStream);
        if (bodyBytes != null && bodyBytes.length > 0) {
            return base64AndMD5(bodyBytes).trim();
        }
        return null;
    }

    /**
     * 将Map转换为用&及=拼接的字符串
     */
    private static String buildMapToSign(Map<String, String> paramMap) {
        StringBuilder builder = new StringBuilder();

        for (Map.Entry<String, String> e : paramMap.entrySet()) {
            if (builder.length() > 0) {
                builder.append('&');
            }

            String key = e.getKey();
            Object value = e.getValue();

            if (value != null) {
                if (value instanceof List) {
                    List list = (List) value;
                    if (list.size() == 0) {
                        builder.append(key);
                    } else {
                        builder.append(key).append("=").append(String.valueOf(list.get(0)));
                    }
                } else if (value instanceof Object[]) {
                    Object[] objs = (Object[]) value;
                    if (objs.length == 0) {
                        builder.append(key);
                    } else {
                        builder.append(key).append("=").append(String.valueOf(objs[0]));
                    }
                } else {
                    builder.append(key).append("=").append(String.valueOf(value));
                }
            }
        }

        return builder.toString();
    }



    /**
     * 组织待计算签名字符串
     *
     * @param headers        HTTP请求头
     * @param resourceToSign Uri+请求参数的签名字符串
     * @param method         HTTP方法
     * @param bodyMd5        Body Md5值
     * @return 待计算签名字符串
     */
    private static String buildStringToSign(Map<String, String> headers, String resourceToSign, String method, String bodyMd5) {
        StringBuilder sb = new StringBuilder();
        sb.append(method).append(LF);
        if (StringUtils.isNotBlank(bodyMd5)) {
            sb.append(bodyMd5);
        }
        sb.append(LF);
        sb.append(buildHeaders(headers));
        sb.append(resourceToSign);

        return sb.toString();
    }

    /**
     * 组织Headers签名签名字符串
     *
     * @param headers HTTP请求头
     * @return Headers签名签名字符串
     */
    private static String buildHeaders(Map<String, String> headers) {
        StringBuilder sb = new StringBuilder();

        TreeMap<String, String> sortMap = new TreeMap<String, String>();
        sortMap.putAll(headers);

        for (Map.Entry<String, String> e : sortMap.entrySet()) {
            if (e.getValue() != null) {
                sb.append(e.getKey().toLowerCase()).append(':').append(e.getValue()).append(LF);
            }
        }
        return sb.toString();
    }

    /**
     * 组织Uri+请求参数的签名字符串
     *
     * @param uri       HTTP请求uri,不包含Query
     * @param paramsMap HTTP请求所有参数（Query+Form参数）
     * @return Uri+请求参数的签名字符串
     */
    private static String buildResource(String uri, Map<String, String> paramsMap) {
        StringBuilder builder = new StringBuilder();

        // uri
        builder.append(uri);

        // Query+Form
        TreeMap<String, String> sortMap = new TreeMap<String, String>();
        sortMap.putAll(paramsMap);


        // 有Query+Form参数
        if (sortMap.size() > 0) {
            builder.append('?');
            builder.append(buildMapToSign(sortMap));
        }

        return builder.toString();
    }

    /**
     * 先进行MD5摘要再进行Base64编码获取摘要字符串
     *
     * @param bytes 待计算字节数组
     * @return
     */
    public static String base64AndMD5(byte[] bytes) {
        if (bytes == null) {
            throw new IllegalArgumentException("bytes can not be null");
        }

        try {
            final MessageDigest md = MessageDigest.getInstance("MD5");
            md.reset();
            md.update(bytes);
            final Base64 base64 = new Base64();

            return new String(base64.encode(md.digest()));
        } catch (final NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm MD5");
        }
    }



}
